package com.elitel.common.mongodb;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLOrderBy;
import com.alibaba.druid.sql.ast.SQLOrderingSpecification;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLIntegerExpr;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.util.JdbcConstants;
import com.elitel.common.utils.XCommonUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.client.*;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * TODO: MongoDB 工具类
 *
 * @since 2020-03-03
 */
@Slf4j
public final class MongoDBUtil {
    // here is change concurrent map to maintain connection pool to support dynamic connection pool
    // key url + database
    // support multiple differenet mongo client
    private static final Map<String, MongoClient> mongoClientPool = new ConcurrentHashMap<>();

    private static class MongoDBUtilHolder {
        private static final MongoDBUtil INSTANCE = new MongoDBUtil();
    }

    public static MongoDBUtil getInstance() {
        return MongoDBUtilHolder.INSTANCE;
    }

    private MongoDBUtil() {
    }

    /**
     * TODO: 解析 SQL 中 where 条件
     *
     * @param where
     * @param whereField
     * @return
     */
    public String parseSQLExpr(SQLExpr where, BasicDBObject whereField) {
        if (where instanceof SQLBinaryOpExpr) {
            SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) where;
            SQLExpr left = sqlBinaryOpExpr.getLeft();
            SQLExpr right = sqlBinaryOpExpr.getRight();
            SQLBinaryOperator operator = sqlBinaryOpExpr.getOperator();

            // 去掉拼接条件
            if (left instanceof SQLIntegerExpr && right instanceof SQLIntegerExpr) {
                return "";
            }

            // 获取 where 键值查询
            String key = parseSQLExpr(left, whereField);
            String value = parseSQLExpr(right, whereField).replace("\"", "").replace("'", "");

            if (!key.equals("") && !value.equals("")) {
                DBObject dbObject = new BasicDBObject();
                switch (operator) {
                    case Like:
                        value = value.replaceAll("^%", "^.*").replaceAll("%$", ".*\\$");
                        if (!value.endsWith("$")) value += "$";
                        if (!value.startsWith("^")) value = "^" + value;
                        Pattern pattern = Pattern.compile(value, Pattern.CASE_INSENSITIVE);
                        whereField.put(key, pattern);
                        break;
                    case Equality:
                        whereField.put(key, value);
                        break;
                    case NotEqual:
                        dbObject.put("$ne", Integer.valueOf(value));
                        whereField.put(key, dbObject);
                        break;
                    case GreaterThan:
                        dbObject.put("$gt", Integer.valueOf(value));
                        whereField.put(key, dbObject);
                        break;
                    case GreaterThanOrEqual:
                        dbObject.put("$gte", Integer.valueOf(value));
                        whereField.put(key, dbObject);
                        break;
                    case LessThan:
                        dbObject.put("$lt", Integer.valueOf(value));
                        whereField.put(key, dbObject);
                        break;
                    case LessThanOrEqual:
                        dbObject.put("$lte", Integer.valueOf(value));
                        whereField.put(key, dbObject);
                        break;
                }
            }
        } else {
            return "";
//      return where.toString();
        }
        return "";
    }

    /**
     * TODO: 将 sql 语句转换为 mongo 查询字段
     *
     * @param sql
     * @return
     */
    public QueryInfo sql2MongoQuery(String sql) {
        QueryInfo queryInfo = null;
        List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL);
        if (sqlStatements.size() == 0) {
            return queryInfo;
        }
        // 默认只解析一条 sql
        SQLStatement stmt = sqlStatements.get(0);
        SQLSelectStatement sqlSelectStatement = (SQLSelectStatement) stmt;

        // 只执行查询解析
        SQLSelectQuery sqlSelectQuery = sqlSelectStatement.getSelect().getQuery();
        if (sqlSelectQuery instanceof SQLSelectQueryBlock) {
            queryInfo = new QueryInfo();
            SQLSelectQueryBlock sqlSelectQueryBlock = (SQLSelectQueryBlock) sqlSelectQuery;

            // 获取查询表名
            SQLTableSource table = sqlSelectQueryBlock.getFrom();
            if (table == null) return null; // 未指定表名
            String collectionName = String.valueOf(table).contains(".") ? String.valueOf(table).split("\\.")[1] : String.valueOf(table);
            queryInfo.setCollectionName(collectionName);

            // 获取展示字段
            List<SQLSelectItem> selectItems = sqlSelectQueryBlock.getSelectList();
            BasicDBObject fields = new BasicDBObject();
            selectItems.forEach(
                    field -> {
                        String fieldName = String.valueOf(field);
						if (fieldName.equals("*"))
                            return;

                        String realFieldName = fieldName.contains(".") ? fieldName.split("\\.")[1] : fieldName;


                        fields.append(realFieldName, 1);
                    });
            queryInfo.setFields(fields);

            // 获取 where 条件
            BasicDBObject where = new BasicDBObject();
             queryInfo.setWhere(where);

            // 获取排序
            BasicDBObject orderBy = new BasicDBObject();
            SQLOrderBy sqlOrderBy = sqlSelectQueryBlock.getOrderBy();
            if (sqlOrderBy != null) {
                sqlOrderBy
                        .getItems()
                        .forEach(
                                o ->
                                        orderBy.append(
                                                o.getExpr().toString(),
                                                o.getType() == SQLOrderingSpecification.ASC ? 1 : -1));
                queryInfo.setOrderBy(orderBy);
            }

            //sqlSelectQueryBlock.getLimit();
        }

        return queryInfo;
    }

    public MongoClient getMongoClient(String url){
        MongoClient mongoClient = mongoClientPool.get(url);
        if(mongoClient == null){
             mongoClient = MongoClients.create(url);
             mongoClientPool.put(url, mongoClient);
        }

        return mongoClient;
    }

    /**
     * TODO: 创建 MongoDB 连接
     *
     * @param url
     * @return
     */
    public MongoDatabase createConnection(String url) {
        MongoClient mongoClient = getMongoClient(url);
        // split / get final element
        String[] urlElements = url.split("/");
        String databaseName = null ;
        if(urlElements != null && urlElements.length > 1){
            // find final element
            databaseName = urlElements[urlElements.length - 1];
        }

        MongoDatabase database = mongoClient.getDatabase(databaseName);
        return database;
    }

    /**
     * TODO: 关闭 MongoDB 连接
     */
    public void close() {
        //if (mongoClient != null) mongoClient.close();
    }

    /**
     * TODO: 获取所有集合
     *
     * @param database
     * @return
     */
    public List<String> getCollections(MongoDatabase database) {
        MongoIterable<String> collectionNames = database.listCollectionNames();
        List<String> listResult = new ArrayList<>();
        for (String name : collectionNames) {
            listResult.add(name);
        }
        return listResult;
    }

    /**
     * TODO: 获取集合中的 字段名称
     *
     * @param queryInfo
     * @return
     */
    public BasicDBObject getFields(QueryInfo queryInfo) {
        BasicDBObject fields = new BasicDBObject();
        // DB db = mongoClient.getDB(queryInfo.getDb().getName());
        // DBCollection collection = db.getCollection(queryInfo.getCollectionName());
        // String map = "function() { \n" + "\tfor (var key in this) { emit(key, null); }\n" + "}";
        // String reduce = "function(key, values){ return key; }";
        // MapReduceCommand mapcmd =
        //         new MapReduceCommand(
        //                 collection, map, reduce, null, MapReduceCommand.OutputType.INLINE, null);
        // MapReduceOutput mapReduceOutput = collection.mapReduce(mapcmd);
        // for (DBObject o : mapReduceOutput.results()) {
        //     fields.append(o.toMap().get("_id").toString(), 1);
        // }
        //
        // here is find one element
        //
        MongoCollection<Document> mongoCollection = queryInfo.getDb().getCollection(queryInfo.getCollectionName());
        List<Document> docList = mongoCollection.find().limit(1).into(new ArrayList<>());
        if(XCommonUtils.isEmpty(docList))
            return fields;

        Map<String, Object> orderColumnObject = new LinkedHashMap<>();
        for(Map.Entry<String,Object> entry: docList.get(0).entrySet()){
            orderColumnObject.put(entry.getKey(), entry.getValue());
        }

        // iterate document all column name
        return fields;
    }

    public Map<String, String> getFirstElementNameAndTypeInCollection(QueryInfo queryInfo){
        Map<String, String> orderColumnObject = new LinkedHashMap<>();

        MongoCollection<Document> mongoCollection = queryInfo.getDb().getCollection(queryInfo.getCollectionName());
        FindIterable<Document> limit = mongoCollection.find().limit(300);
        for (Document document : limit) {
            Set<Map.Entry<String, Object>> entries = document.entrySet();
            for (Map.Entry<String, Object> entry : entries) {
                String key = entry.getKey();
                Object value = entry.getValue();
                String valueType = value== null ? "null" : value.getClass().getSimpleName();
                orderColumnObject.put(key, valueType);
            }
        }

        return orderColumnObject ;
    }

    /**
     * TODO: 获取总条数
     *
     * @param queryInfo
     * @return
     */
    public Long findCount(QueryInfo queryInfo) {
        return queryInfo
                .getDb()
                .getCollection(queryInfo.getCollectionName())
                .countDocuments(queryInfo.getWhere());
    }

    /**
     * TODO: 分页获取 MongoDB 中的数据
     *
     * @param queryInfo
     * @param skip
     * @param limit
     * @return
     */
    public List<Map<String, Object>> find(QueryInfo queryInfo, int skip, int limit) {
        MongoCursor<Document> iterator = find(queryInfo).limit(limit).skip(skip).iterator();
        return convertResult(iterator);
    }

    /**
     * TODO: MongoDB 查询所有数据
     *
     * @param queryInfo
     * @return
     */
    public List<Map<String, Object>> findAll(QueryInfo queryInfo) {
        MongoCursor<Document> iterator = find(queryInfo).iterator();
        return convertResult(iterator);
    }

    /**
     * TODO: MongoDB 基础查询
     *
     * @param queryInfo
     * @return
     */
    private FindIterable<Document> find(QueryInfo queryInfo) {
        FindIterable<Document> iterable = queryInfo
                        .getDb()
                        .getCollection(queryInfo.getCollectionName())
                        .find(queryInfo.getWhere())
                        .projection(queryInfo.getFields());
        // is pageable
        if(queryInfo.isPageable()){
            iterable
                .skip(queryInfo.getSkipPageNo())
                .limit(queryInfo.getPageSize());
        }
        // 对查询结果排序
        if (queryInfo.getOrderBy() != null) {
            iterable = iterable.sort(queryInfo.getOrderBy());
        }
        return iterable;
    }

    /**
     * TODO: 转换 MongoDB 结果
     *
     * @param iterator
     * @return
     */
    private List<Map<String, Object>> convertResult(MongoCursor<Document> iterator) {
        List<Map<String, Object>> list = new ArrayList<>();
        while (iterator.hasNext()) {
            Document next = iterator.next();
            Map<String, Object> map = new HashMap<>();
            next.keySet().forEach(key ->
                    {
                        Object value = next.get(key);
                        if(value == null)
                            value = "";
                        // judge is date type
                        boolean dateFullParse = false;


                        if(value.getClass().getSimpleName().equals("Date")){
                            try{
                                if(!dateFullParse){
                                    value = XCommonUtils.formatDateToSecond((Date)value);
                                }else{
                                    value = XCommonUtils.formatDateToMillisecond((Date)value);
                                }
                            }catch(Exception e){
                                try{
                                    if(!dateFullParse){
                                        value = XCommonUtils.formatDateToMillisecond((Date)value);
                                    }
                                    dateFullParse = true;
                                }catch(Exception e2){
                                    //log.error("Can't parse date type from mongodb, value = " + value  + " exceptionMessage:" + e2.getMessage());
                                }
                                // don't change format
                                //e.printStackTrace();
                                value = "";
                            }
                        }
                        map.put(key, value.toString());
                    }
            );
            list.add(map);
        }
        return list;
    }


}
